package androidgame.teslawars.game;

import java.util.ArrayList;
import java.util.Iterator;

import androidgame.framework.math.Vector2;

// This class will be extended for each map we implement. Each extension must define the worldWidth and worldHeight,
// and what level it is.
// @author bend2, dicarlo2

public abstract class Level {

	public enum Decision {
		CONFIRM, CANCEL, END_TURN, END_TURN_COMPLETE, NEXT_UNIT
	} // Used for updating game logic based on a player decision.

	/*
	 * The progression of the game resembles chess: Player selects unit to move
	 * --> Player selects where to move unit --> Player selects who/where to
	 * attack and then repeats the process for the other player.
	 * 
	 * For the states with Confirm in their name, an appropriate confirmation
	 * dialog needs to be displayed. (e.g. during Player1ConfirmMove, the player
	 * needs to be able to select whether to confirm the move or to cancel and
	 * return to Player1SelectMove)
	 * 
	 * At any state a touch on a tile will transition to the appropriate state,
	 * regardless of the dialog box shown. For example, if we are in the
	 * CONFIRM_ATTACK state and another friendly unit with move available is
	 * selected, we will instantly transition to the SELECT_MOVE state.
	 */
	public enum GameState {
		SELECT_UNIT, SELECT_MOVE, CONFIRM_MOVE, SELECT_ATTACK, CONFIRM_ATTACK, END_TURN, GAME_OVER
		// Player2SelectUnit, Player2SelectMove, Player2ConfirmMove,
		// Player2SelectAttack, Player2ConfirmAttack
	}

	public enum Player {
		PLAYER1, PLAYER2
	}

	public interface LevelListener {
		// define the sounds you want used here. Example:
		public void hit();
		// then simply call lisener.hit() every time you want that sound to be
		// played.
	}

	protected int worldWidth;
	protected int worldHeight;
	protected LevelListener listener;
	protected Tile[][] grid;
	protected Unit selectedUnit; // The allied unit currently selected by the
								// player.
	protected Tile selectedTile; // The tile most recently (or currently) occupied
								// by the cursor (or player's selection point)
	protected GameState state;
	protected ArrayList<Unit> player1UnitList;
	protected ArrayList<Unit> player2UnitList;
	protected ArrayList<Unit> currentPlayerUnitList;
	protected ArrayList<Tile> highlightedTiles;
	protected Tile previousUnitLocation;
	protected Player playerTurn;

	// created during startup to save resources.

	public Level(LevelListener listener) {
		this.listener = listener;
		highlightedTiles = new ArrayList<Tile>(25);
		state = GameState.SELECT_UNIT;
		selectedUnit = null;
		selectedTile = null;
		previousUnitLocation = null;
		playerTurn = Player.PLAYER1;
		// Other things to initialize when the World is created.
	}

	public void update(Vector2 touchPoint) {
		if (touchPoint.x >= 0 && touchPoint.x < worldWidth && touchPoint.y >= 0
				&& touchPoint.y < worldHeight)
			select(grid[(int) touchPoint.x][(int) touchPoint.y]);
	}

	/*
	 * This method will only need to be called (and will only do anything)
	 * during the gamestates where decision confirmation is needed. (i.e. the
	 * PlayerConfirmAttack/Move states)
	 */
	public void update(Decision decision) {

		int startIndex;
		int index;

		// If the NEXT string is touched then we try and find a unit with move
		// or attack available.
		// If this is not possible, we simply return.
		if (decision == Decision.NEXT_UNIT) {
			if (selectedUnit != null && currentPlayerUnitList.contains(selectedUnit)) {
				startIndex = currentPlayerUnitList.indexOf(selectedUnit);
				if (startIndex == currentPlayerUnitList.size() - 1)
					index = 0;
				else
					index = startIndex + 1;
				while (index != startIndex) {
					Unit unit = currentPlayerUnitList.get(index);
					if (unit.canMove || unit.canAttack) {
						if (state == GameState.CONFIRM_MOVE)
							moveUnit(selectedUnit, previousUnitLocation);
						selectedTile = grid[unit.x][unit.y];
						selectedUnit = selectedTile.unit;
						unhighlightAllTiles();
						if (unit.canMove) {
							highlightReachableTiles(selectedUnit);
							state = GameState.SELECT_MOVE;
						} else {
							highlightAttackableUnits(selectedUnit);
							setTileHighlight(selectedTile, true);
							state = GameState.SELECT_ATTACK;
						}
						return;
					}
					if (index == currentPlayerUnitList.size() - 1)
						index = 0;
					else
						index++;
				}

			} else {
				for (Unit unit : currentPlayerUnitList) {
					if (unit.canMove || unit.canAttack) {
						unhighlightAllTiles();
						selectedUnit = unit;
						selectedTile = grid[unit.x][unit.y];
						if (unit.canMove) {
							highlightReachableTiles(selectedUnit);
							state = GameState.SELECT_MOVE;
						} else {
							highlightAttackableUnits(selectedUnit);
							setTileHighlight(selectedTile, true);
							state = GameState.SELECT_ATTACK;
						}
					}
				}
			}
		}

		// Updates the world based on which option was selected from the move
		// confirmation dialog.
		
		if (decision == Decision.CONFIRM){
			if (state == GameState.CONFIRM_MOVE){
				unhighlightAllTiles();
				highlightAttackableUnits(selectedUnit);
				if (highlightedTiles.isEmpty()) {
					state = GameState.SELECT_UNIT;
				}
				else {
					selectedUnit.canAttack = true;
					state = GameState.SELECT_ATTACK;
				}
				selectedUnit.canMove = false;
			} else if (state == GameState.CONFIRM_ATTACK) {
				if (attack(selectedUnit, selectedTile.unit)) {
					if (player1UnitList.contains(selectedTile.unit)) {
						player1UnitList.remove(selectedTile.unit);
						if (player1UnitList.isEmpty()) {
							state = GameState.GAME_OVER;
							return;
						}
					} else if (player2UnitList.contains(selectedTile.unit)) {
						player2UnitList.remove(selectedTile.unit);
						if (player2UnitList.isEmpty()) {
							state = GameState.GAME_OVER;
							return;
						}
					}
					selectedTile.unit = null;
				}
				unhighlightAllTiles();
				selectedUnit.canAttack = false;
				state = GameState.SELECT_UNIT;
			}
			setTileHighlight(selectedTile, true);
		}
		
		if (decision == Decision.CANCEL) {
			if (state == GameState.CONFIRM_MOVE){
				moveUnit(selectedUnit, previousUnitLocation);
				highlightReachableTiles(selectedUnit);
				state = GameState.SELECT_MOVE;
			} else if (state == GameState.CONFIRM_ATTACK) {
				highlightAttackableUnits(selectedUnit);
				setTileHighlight(grid[selectedUnit.x][selectedUnit.y], true);
				state = GameState.SELECT_ATTACK;
			}
		}
		
		// Ends the turn and switches player control.
		if (decision == Decision.END_TURN) {
			switch (playerTurn) {
			case PLAYER1:
				currentPlayerUnitList = player2UnitList;
				playerTurn = Player.PLAYER2;
				break;
			case PLAYER2:
				currentPlayerUnitList = player1UnitList;
				playerTurn = Player.PLAYER1;
				break;
			}
			for (Unit unit : currentPlayerUnitList)
				unit.canMove = true;
			state = GameState.END_TURN;
		}

		if (decision == Decision.END_TURN_COMPLETE)
			state = GameState.SELECT_UNIT;

	}

	/*
	 * As described above, contains every possible game state and where to
	 * transition no matter what tile is selected.
	 * 
	 * Method called when user selects a specific tile. Works closely in tandem
	 * with update(Decision)
	 * 
	 * The reference for the tile returned by the most recent call of this
	 * method is saved in the local variable: selectedTile
	 */

	private void select(Tile tile) {
		selectedTile = tile;

		if (state == GameState.SELECT_UNIT) {
			decideSelectState(tile);
		}

		else if (state == GameState.SELECT_MOVE) {
			if (tile.isHighlighted) {
				moveUnit(selectedUnit, tile);
				unhighlightAllTiles();
				setTileHighlight(selectedTile, true);
				state = GameState.CONFIRM_MOVE;
			} else {
				decideSelectState(tile);
			}

		} else if (state == GameState.CONFIRM_MOVE) {
			if (selectedTile == grid[selectedUnit.x][selectedUnit.y])
				;
			else {
				moveUnit(selectedUnit, previousUnitLocation);
				decideSelectState(tile);
			}
		} else if (state == GameState.SELECT_ATTACK) {
			if (tile.isHighlighted && tile.unit != selectedUnit) {
				unhighlightAllTiles();
				setTileHighlight(tile, true);
				setTileHighlight(
						grid[selectedUnit.x][selectedUnit.y],
						true);
				state = GameState.CONFIRM_ATTACK;
			} else {
				decideSelectState(tile);
			}
		} else if (state == GameState.CONFIRM_ATTACK) {
			if (selectedTile.unit != null && !currentPlayerUnitList.contains(selectedTile.unit)){
				unhighlightAllTiles();
				highlightAttackableUnits(selectedUnit);
				if (selectedTile.isHighlighted){
					unhighlightAllTiles();
					setTileHighlight(selectedTile, true);
					setTileHighlight(grid[selectedUnit.x][selectedUnit.y],true);
				} else {
					decideSelectState(tile);
				}
			} else {
				decideSelectState(tile);
			}
		}
	}
	
	// Helper method to reuse code. If a transition to a select state is needed, this is called and the appropriate state is changed to the appropriate
	// select state.
	
	private void decideSelectState(Tile tile) {
		unhighlightAllTiles();
		if (tile.unit == null) {
			setTileHighlight(tile, true);
			state = GameState.SELECT_UNIT;
		}
		else if (!currentPlayerUnitList.contains(tile.unit)) {
			setTileHighlight(tile, true);
			state = GameState.SELECT_UNIT;
		}
		else if (currentPlayerUnitList.contains(tile.unit)) {
			if (tile.unit.canMove) {
				selectedUnit = selectedTile.unit;
				highlightReachableTiles(selectedUnit);
				state = GameState.SELECT_MOVE;
			} else if (tile.unit.canAttack) {
				selectedUnit = selectedTile.unit;
				highlightAttackableUnits(selectedUnit);
				setTileHighlight(grid[selectedUnit.x][selectedUnit.y], true);
				state = GameState.SELECT_ATTACK;
			} else {
				setTileHighlight(tile, true);
				state = GameState.SELECT_UNIT;
			}
		}
	}

	// Wrapper for highlighting tiles for movement
	private void highlightReachableTiles(Unit unit) {
		highlightSpacesInRange(unit.x, unit.y,
				unit.moveRange, unit);
		Iterator<Tile> it = highlightedTiles.iterator();
		while (it.hasNext()) {
			Tile tile = it.next();
			if (tile.unit != null && tile.unit != selectedUnit) {
				it.remove();
				tile.isHighlighted = false;
			}
		}
	}

	// Wrapper for highlighting tiles for attack
	private void highlightAttackableUnits(Unit unit) {
		highlightSpacesInRange(unit.x, unit.y,
				unit.attackRange, null);
		Iterator<Tile> it = highlightedTiles.iterator();
		while (it.hasNext()) {
			Tile tile = it.next();
			if (tile.unit == null || currentPlayerUnitList.contains(tile.unit)) {
				it.remove();
				tile.isHighlighted = false;
			}
		}
	}

	// Recursive search function
	public void highlightSpacesInRange(int x, int y, int range, Unit unit) {
		int moveCost = 1;
		if ((x < 0 || x >= grid.length || y < 0 || y >= grid[0].length)
				|| range < 0)
			return;
		if (unit != null) {
			moveCost = unit.moveCost(grid[x][y]);
		}
		setTileHighlight(grid[x][y], true);
		highlightSpacesInRange(x + 1, y, range - moveCost, unit);
		highlightSpacesInRange(x - 1, y, range - moveCost, unit);
		highlightSpacesInRange(x, y + 1, range - moveCost, unit);
		highlightSpacesInRange(x, y - 1, range - moveCost, unit);
	}

	public void moveUnit(Unit unit, Tile tile) {
		previousUnitLocation = grid[unit.x][unit.y];

		unit.x = tile.x;
		unit.y = tile.y;
		tile.unit = unit;
		if (previousUnitLocation != tile)
			previousUnitLocation.unit = null;
	}

	/*
	 * Assigns damage onto target. Returns true if target unit was destroyed.
	 */
	private boolean attack(Unit attacker, Unit target) {
		int damage = attacker.attack(target); // also can calculate in terrain
												// bonuses.
		damage = damage - target.getBonusDefenseValue(grid[target.x][target.y]);
		if (damage > 0) {
			target.health -= damage;
		}
		listener.hit();
		return target.health <= 0;
	}

	/*
	 * Sets whether a tile should be highlighted by calling
	 * Tile.setHighlighted() as well as adjusting the local Arraylist of
	 * highlighted tiles appropriately
	 */
	public void setTileHighlight(Tile tile, boolean shouldBeHighlighted) {
		if (shouldBeHighlighted && !tile.isHighlighted) {
			tile.isHighlighted = true;
			highlightedTiles.add(tile);
		} else if (!shouldBeHighlighted && tile.isHighlighted) {
			tile.isHighlighted = false;
			highlightedTiles.remove(tile);
		}
	}

	public void unhighlightAllTiles() {
		Iterator<Tile> it = highlightedTiles.iterator();
		while (it.hasNext()) {
			it.next().isHighlighted = false;
			it.remove();
		}
	}

	/*
	 * Assigns the references for each unit to their respective tiles. Called on
	 * level startup.
	 */
	public void addUnitsToTiles() {
		for (Unit unit : player1UnitList) {
				grid[unit.x][unit.y].unit = unit;
		}
		for (Unit unit : player2UnitList) {
				grid[unit.x][unit.y].unit = unit;
		}
	}

	public int getWorldWidth() {
		return worldWidth;
	}

	public int getWorldHeight() {
		return worldHeight;
	}

	public Vector2 getCenter() {
		return new Vector2(worldWidth / 2, worldHeight / 2);
	}
	
	public GameState getState() {
		return state;
	}

	public ArrayList<Unit> getPlayer1UnitList() {
		return player1UnitList;
	}

	public ArrayList<Unit> getPlayer2UnitList() {
		return player2UnitList;
	}

	public ArrayList<Unit> getCurrentPlayerUnitList() {
		return currentPlayerUnitList;
	}

	public ArrayList<Tile> getHighlightedTiles() {
		return highlightedTiles;
	}
	
	public Unit getSelectedUnit() {
		return selectedUnit;
	}
	
	public Tile getSelectedTile() {
		return selectedTile;
	}

	public Tile getTile(Unit unit) {
		return grid[unit.x][unit.y];
	}

	public Tile getTile(int x, int y) {
		return grid[x][y];
	}

	public Player getPlayerTurn() {
		return playerTurn;
	}
	
	public Tile[][] getGrid() {
		return grid;
	}

}
